diff --git a/patchwork/admin.py b/patchwork/admin.py
index f9a94c6f5c07..c3d45240f1eb 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -14,6 +14,7 @@ from patchwork.models import Comment
 from patchwork.models import CoverLetter
 from patchwork.models import DelegationRule
 from patchwork.models import Patch
+from patchwork.models import PatchRelation
 from patchwork.models import Person
 from patchwork.models import Project
 from patchwork.models import Series
@@ -174,3 +175,10 @@ class TagAdmin(admin.ModelAdmin):
 
 
 admin.site.register(Tag, TagAdmin)
+
+
+class PatchRelationAdmin(admin.ModelAdmin):
+    model = PatchRelation
+
+
+admin.site.register(PatchRelation, PatchRelationAdmin)
diff --git a/patchwork/api/event.py b/patchwork/api/event.py
index a066faaec63b..d7685f4c138c 100644
--- a/patchwork/api/event.py
+++ b/patchwork/api/event.py
@@ -42,6 +42,8 @@ class EventSerializer(ModelSerializer):
                                              'current_state'],
         Event.CATEGORY_PATCH_DELEGATED: ['patch', 'previous_delegate',
                                          'current_delegate'],
+        Event.CATEGORY_PATCH_RELATION_CHANGED: ['patch', 'previous_relation',
+                                                'current_relation'],
         Event.CATEGORY_CHECK_CREATED: ['patch', 'created_check'],
         Event.CATEGORY_SERIES_CREATED: ['series'],
         Event.CATEGORY_SERIES_COMPLETED: ['series'],
@@ -68,7 +70,8 @@ class EventSerializer(ModelSerializer):
         model = Event
         fields = ('id', 'category', 'project', 'date', 'actor', 'patch',
                   'series', 'cover', 'previous_state', 'current_state',
-                  'previous_delegate', 'current_delegate', 'created_check')
+                  'previous_delegate', 'current_delegate', 'created_check',
+                  'previous_relation', 'current_relation',)
         read_only_fields = fields
         versioned_fields = {
             '1.2': ('actor', ),
diff --git a/patchwork/migrations/0040_add_related_patches.py b/patchwork/migrations/0040_add_related_patches.py
new file mode 100644
index 000000000000..f4d811fedddd
--- /dev/null
+++ b/patchwork/migrations/0040_add_related_patches.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-01-15 23:15
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('patchwork', '0039_unique_series_references'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='PatchRelation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='event',
+            name='category',
+            field=models.CharField(choices=[(b'cover-created', b'Cover Letter Created'), (b'patch-created', b'Patch Created'), (b'patch-completed', b'Patch Completed'), (b'patch-state-changed', b'Patch State Changed'), (b'patch-delegated', b'Patch Delegate Changed'), (b'patch-relation-changed', b'Patch Relation Changed'), (b'check-created', b'Check Created'), (b'series-created', b'Series Created'), (b'series-completed', b'Series Completed')], db_index=True, help_text=b'The category of the event.', max_length=25),
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='current_relation',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='patchwork.PatchRelation'),
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='previous_relation',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='patchwork.PatchRelation'),
+        ),
+        migrations.AddField(
+            model_name='patch',
+            name='related',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patches', related_query_name='patch', to='patchwork.PatchRelation'),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index be71d4077892..4f7186d3ec10 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -447,6 +447,12 @@ class Patch(Submission):
         default=None, null=True,
         help_text='The number assigned to this patch in the series')
 
+    # related patches metadata
+
+    related = models.ForeignKey(
+        'PatchRelation', null=True, blank=True, on_delete=models.SET_NULL,
+        related_name='patches', related_query_name='patch')
+
     objects = PatchManager()
 
     @staticmethod
@@ -863,6 +869,19 @@ class BundlePatch(models.Model):
         ordering = ['order']
 
 
+@python_2_unicode_compatible
+class PatchRelation(models.Model):
+
+    def __str__(self):
+        patches = self.patches.all()
+        if not patches:
+            return '<Empty>'
+        name = ', '.join(patch.name for patch in patches[:10])
+        if len(name) > 60:
+            name = name[:60] + '...'
+        return name
+
+
 @python_2_unicode_compatible
 class Check(models.Model):
 
@@ -928,6 +947,7 @@ class Event(models.Model):
     CATEGORY_PATCH_COMPLETED = 'patch-completed'
     CATEGORY_PATCH_STATE_CHANGED = 'patch-state-changed'
     CATEGORY_PATCH_DELEGATED = 'patch-delegated'
+    CATEGORY_PATCH_RELATION_CHANGED = 'patch-relation-changed'
     CATEGORY_CHECK_CREATED = 'check-created'
     CATEGORY_SERIES_CREATED = 'series-created'
     CATEGORY_SERIES_COMPLETED = 'series-completed'
@@ -937,6 +957,7 @@ class Event(models.Model):
         (CATEGORY_PATCH_COMPLETED, 'Patch Completed'),
         (CATEGORY_PATCH_STATE_CHANGED, 'Patch State Changed'),
         (CATEGORY_PATCH_DELEGATED, 'Patch Delegate Changed'),
+        (CATEGORY_PATCH_RELATION_CHANGED, 'Patch Relation Changed'),
         (CATEGORY_CHECK_CREATED, 'Check Created'),
         (CATEGORY_SERIES_CREATED, 'Series Created'),
         (CATEGORY_SERIES_COMPLETED, 'Series Completed'),
@@ -952,7 +973,7 @@ class Event(models.Model):
     # event metadata
 
     category = models.CharField(
-        max_length=20,
+        max_length=25,
         choices=CATEGORY_CHOICES,
         db_index=True,
         help_text='The category of the event.')
@@ -1000,6 +1021,15 @@ class Event(models.Model):
         User, related_name='+', null=True, blank=True,
         on_delete=models.CASCADE)
 
+    # fields for 'patch-relation-changed-changed' events
+
+    previous_relation = models.ForeignKey(
+        PatchRelation, related_name='+', null=True, blank=True,
+        on_delete=models.CASCADE)
+    current_relation = models.ForeignKey(
+        PatchRelation, related_name='+', null=True, blank=True,
+        on_delete=models.CASCADE)
+
     # fields or 'patch-check-created' events
 
     created_check = models.ForeignKey(
diff --git a/patchwork/signals.py b/patchwork/signals.py
index 73ddfa5e35ee..3a2f0fbdd3a4 100644
--- a/patchwork/signals.py
+++ b/patchwork/signals.py
@@ -134,6 +134,30 @@ def create_patch_delegated_event(sender, instance, raw, **kwargs):
     create_event(instance, orig_patch.delegate, instance.delegate)
 
 
+@receiver(pre_save, sender=Patch)
+def create_patch_relation_changed_event(sender, instance, raw, **kwargs):
+
+    def create_event(patch, before, after):
+        return Event.objects.create(
+            category=Event.CATEGORY_PATCH_RELATION_CHANGED,
+            project=patch.project,
+            actor=getattr(patch, '_edited_by', None),
+            patch=patch,
+            previous_relation=before,
+            current_relation=after)
+
+    # don't trigger for items loaded from fixtures or new items
+    if raw or not instance.pk:
+        return
+
+    orig_patch = Patch.objects.get(pk=instance.pk)
+
+    if orig_patch.related == instance.related:
+        return
+
+    create_event(instance, orig_patch.related, instance.related)
+
+
 @receiver(pre_save, sender=Patch)
 def create_patch_completed_event(sender, instance, raw, **kwargs):
 
diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
index 77a2711ab5b4..978559b8726b 100644
--- a/patchwork/templates/patchwork/submission.html
+++ b/patchwork/templates/patchwork/submission.html
@@ -110,6 +110,43 @@ function toggle_div(link_id, headers_id, label_show, label_hide)
   </td>
  </tr>
 {% endif %}
+{% if submission.related %}
+ <tr>
+  <th>Related</th>
+  <td>
+   <a id="togglerelated"
+      href="javascript:toggle_div('togglerelated', 'related')"
+   >show</a>
+   <div id="related" class="submissionlist" style="display:none;">
+    <ul>
+     {% for sibling in related_same_project %}
+      <li>
+       {% if sibling.id != submission.id %}
+        <a href="{% url 'patch-detail' project_id=project.linkname msgid=sibling.url_msgid %}">
+         {{ sibling.name|default:"[no subject]"|truncatechars:100 }}
+        </a>
+       {% endif %}
+      </li>
+     {% endfor %}
+     {% if related_different_project %}
+      <a id="togglerelatedoutside"
+         href="javascript:toggle_div('togglerelatedoutside', 'relatedoutside', 'show from other projects')"
+      >show from other projects</a>
+      <div id="relatedoutside" class="submissionlist" style="display:none;">
+       {% for sibling in related_outside %}
+        <li>
+         <a href="{% url 'patch-detail' project_id=sibling.project.linkname msgid=sibling.url_msgid %}">
+          {{ sibling.name|default:"[no subject]"|truncatechars:100 }}
+         </a> (in {{ sibling.project }})
+        </li>
+       {% endfor %}
+      </div>
+     {% endif %}
+    </ul>
+   </div>
+  </td>
+ </tr>
+{% endif %}
 </table>
 
 <div class="patchforms">
diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py
index f34053ce57da..e32ff0bff5f5 100644
--- a/patchwork/views/patch.py
+++ b/patchwork/views/patch.py
@@ -110,12 +110,25 @@ def patch_detail(request, project_id, msgid):
     comments = comments.only('submitter', 'date', 'id', 'content',
                              'submission')
 
+    if patch.related:
+        related_same_project = \
+            patch.related.patches.only('name', 'msgid', 'project', 'related')
+        # avoid a second trip out to the db for info we already have
+        related_different_project = \
+            [related_patch for related_patch in related_same_project
+             if related_patch.project_id != patch.project_id]
+    else:
+        related_same_project = []
+        related_different_project = []
+
     context['comments'] = comments
     context['checks'] = patch.check_set.all().select_related('user')
     context['submission'] = patch
     context['patchform'] = form
     context['createbundleform'] = createbundleform
     context['project'] = patch.project
+    context['related_same_project'] = related_same_project
+    context['related_different_project'] = related_different_project
 
     return render(request, 'patchwork/submission.html', context)
 
