diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index e9064bb..72e9f31 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -690,6 +690,18 @@ class ModelAdmin(BaseModelAdmin):
         """
         formset.save()
 
+    def save_related(self, request, form, formsets, change):
+        """
+        Given the ``HttpRequest``, the parent ``ModelForm`` instance, the list
+        of inline formsets and a boolean value based on whether the parent is
+        being added or changed, save the related objects to the database. Note
+        that at this point save_form() and save_model() have already been
+        called.
+        """
+        form.save_m2m()
+        for formset in formsets:
+            self.save_formset(request, form, formset, change=change)
+
     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
         opts = self.model._meta
         app_label = opts.app_label
@@ -893,11 +905,8 @@ class ModelAdmin(BaseModelAdmin):
                                   prefix=prefix, queryset=inline.queryset(request))
                 formsets.append(formset)
             if all_valid(formsets) and form_validated:
-                self.save_model(request, new_object, form, change=False)
-                form.save_m2m()
-                for formset in formsets:
-                    self.save_formset(request, form, formset, change=False)
-
+                self.save_model(request, new_object, form, False)
+                self.save_related(request, form, formsets, False)
                 self.log_addition(request, new_object)
                 return self.response_add(request, new_object)
         else:
@@ -995,11 +1004,8 @@ class ModelAdmin(BaseModelAdmin):
                 formsets.append(formset)
 
             if all_valid(formsets) and form_validated:
-                self.save_model(request, new_object, form, change=True)
-                form.save_m2m()
-                for formset in formsets:
-                    self.save_formset(request, form, formset, change=True)
-
+                self.save_model(request, new_object, form, True)
+                self.save_related(request, form, formsets, True)
                 change_message = self.construct_change_message(request, form, formsets)
                 self.log_change(request, new_object, change_message)
                 return self.response_change(request, new_object)
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index e0ddd4a..e5560c2 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -949,6 +949,16 @@ templates used by the :class:`ModelAdmin` views:
                     instance.save()
                 formset.save_m2m()
 
+.. method:: ModelAdmin.save_related(self, request, form, formsets, change)
+
+    .. versionadded:: 1.4
+
+    The ``save_related`` method is given the ``HttpRequest``, the parent
+    ``ModelForm`` instance, the list of inline formsets and a boolean value
+    based on whether the parent is being added or changed. Here you can do any
+    pre- or post-save operations for objects related to the parent. Note
+    that at this point the parent object and its form have already been saved.
+
 .. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
 
     .. versionadded:: 1.2
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index ddbcda9..affe361 100644
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -58,6 +58,13 @@ elements of the :attr:`~django.contrib.admin.ModelAdmin.ordering` attribute, and
 sorting on multiple columns by clicking on headers is designed to work similarly
 to how desktop GUIs do it.
 
+``ModelAdmin.save_related()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A new :meth:`~django.contrib.admin.ModelAdmin.save_related` hook was added to
+:mod:`~django.contrib.admin.ModelAdmin` to ease the customization of how
+related objects are saved in the admin.
+
 Tools for cryptographic signing
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index 52d96a9..8d79f44 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -391,6 +391,14 @@ class ParentAdmin(admin.ModelAdmin):
     model = Parent
     inlines = [ChildInline]
 
+    def save_related(self, request, form, formsets, change):
+        super(ParentAdmin, self).save_related(request, form, formsets, change)
+        first_name, last_name = form.instance.name.split()
+        for child in form.instance.child_set.all():
+            if len(child.name.split()) < 2:
+                child.name = child.name + ' ' + last_name
+                child.save()
+
 class EmptyModel(models.Model):
     def __unicode__(self):
         return "Primary key = %s" % self.id
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index 25056b5..7128f04 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -38,7 +38,7 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel,
     Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
     Question, Answer, Inquisition, Actor, FoodDelivery,
     RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory,
-    ComplexSortedPerson)
+    ComplexSortedPerson, Parent, Child)
 
 
 class AdminViewBasicTest(TestCase):
@@ -3113,3 +3113,50 @@ class DateHierarchyTests(TestCase):
             self.assert_non_localized_year(response, 2000)
             self.assert_non_localized_year(response, 2003)
             self.assert_non_localized_year(response, 2005)
+
+class AdminCustomSaveRelatedTests(TestCase):
+    """
+    Ensure that one can easily customize the way related objects are saved.
+    Refs #16115.
+    """
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+
+    def test_should_be_able_to_edit_related_objects_on_add_view(self):
+        post = {
+            'child_set-TOTAL_FORMS': '3',
+            'child_set-INITIAL_FORMS': '0',
+            'name': 'Josh Stone',
+            'child_set-0-name': 'Paul',
+            'child_set-1-name': 'Catherine',
+        }
+        response = self.client.post('/test_admin/admin/admin_views/parent/add/', post)
+        self.assertEqual(1, Parent.objects.count())
+        self.assertEqual(2, Child.objects.count())
+
+        children_names = list(Child.objects.order_by('name').values_list('name', flat=True))
+
+        self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
+        self.assertEqual([u'Catherine Stone', u'Paul Stone'], children_names)
+
+    def test_should_be_able_to_edit_related_objects_on_change_view(self):
+        parent = Parent.objects.create(name='Josh Stone')
+        paul = Child.objects.create(parent=parent, name='Paul')
+        catherine = Child.objects.create(parent=parent, name='Catherine')
+        post = {
+            'child_set-TOTAL_FORMS': '5',
+            'child_set-INITIAL_FORMS': '2',
+            'name': 'Josh Stone',
+            'child_set-0-name': 'Paul',
+            'child_set-0-id': paul.id,
+            'child_set-1-name': 'Catherine',
+            'child_set-1-id': catherine.id,
+        }
+        response = self.client.post('/test_admin/admin/admin_views/parent/%s/' % parent.id, post)
+
+        children_names = list(Child.objects.order_by('name').values_list('name', flat=True))
+
+        self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
+        self.assertEqual([u'Catherine Stone', u'Paul Stone'], children_names)
