diff --git a/wagtail/admin/tests/pages/test_content_type_use_view.py b/wagtail/admin/tests/pages/test_content_type_use_view.py
index d9e00cb619..d9ed598bfa 100644
--- a/wagtail/admin/tests/pages/test_content_type_use_view.py
+++ b/wagtail/admin/tests/pages/test_content_type_use_view.py
@@ -79,7 +79,7 @@ class TestContentTypeUse(WagtailTestUtils, TestCase):
         self.assertContains(response, delete_url)
         self.assertContains(response, "data-bulk-action-select-all-checkbox")
 
-        with self.assertNumQueries(38):
+        with self.assertNumQueries(33):
             self.client.get(request_url)
 
     def test_content_type_use_results(self):
diff --git a/wagtail/admin/ui/tables/pages.py b/wagtail/admin/ui/tables/pages.py
index dde536bcb4..59b00e0db7 100644
--- a/wagtail/admin/ui/tables/pages.py
+++ b/wagtail/admin/ui/tables/pages.py
@@ -56,6 +56,8 @@ class ParentPageColumn(Column):
     cell_template_name = "wagtailadmin/pages/listing/_parent_page_cell.html"
 
     def get_value(self, instance):
+        if parent := getattr(instance, "_parent_page", None):
+            return parent
         return instance.get_parent()
 
 
diff --git a/wagtail/admin/views/pages/listing.py b/wagtail/admin/views/pages/listing.py
index fb55b4e274..ca3980ee65 100644
--- a/wagtail/admin/views/pages/listing.py
+++ b/wagtail/admin/views/pages/listing.py
@@ -265,6 +265,14 @@ class PageListingMixin:
         kwargs["actions_next_url"] = self.index_url
         return kwargs
 
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+
+        if any(isinstance(column, ParentPageColumn) for column in self.columns):
+            Page.objects.annotate_parent_page(context["object_list"])
+
+        return context
+
 
 class IndexView(PageListingMixin, generic.IndexView):
     template_name = "wagtailadmin/pages/index.html"
@@ -439,19 +447,12 @@ class ExplorableIndexView(IndexView):
         context = super().get_context_data(**kwargs)
 
         if self.is_searching:
-            # postprocess this page of results to annotate each result with its parent page
-            parent_page_paths = {
-                page.path[: -page.steplen] for page in context["object_list"]
-            }
-            parent_pages_by_path = {
-                page.path: page
-                for page in Page.objects.filter(path__in=parent_page_paths).specific()
-            }
+            Page.objects.annotate_parent_page(context["object_list"])
             for page in context["object_list"]:
-                parent_page = parent_pages_by_path.get(page.path[: -page.steplen])
                 # add annotation if parent page is found and is not the currently viewed parent
-                if parent_page and parent_page != self.parent_page:
-                    page.annotated_parent_page = parent_page
+                # to be used by PageTitleColumn instead of a dedicated ParentPageColumn
+                if page._parent_page and page._parent_page != self.parent_page:
+                    page.annotated_parent_page = page._parent_page
 
         context.update(
             {
diff --git a/wagtail/models/__init__.py b/wagtail/models/__init__.py
index 7d451d58ce..940a276627 100644
--- a/wagtail/models/__init__.py
+++ b/wagtail/models/__init__.py
@@ -235,6 +235,36 @@ class BasePageManager(models.Manager):
 
         return self.get(path=common_parent_path)
 
+    def annotate_parent_page(self, pages):
+        """
+        Annotates each page with its parent page. This is implemented as a
+        manager-only method instead of a QuerySet method so it can be used with
+        search results.
+
+        If given a QuerySet, this method will evaluate it. Only use this method
+        when you are ready to consume the queryset, e.g. after pagination has
+        been applied. This is typically done in the view's `get_context_data`
+        using `context["object_list"]`.
+
+        This method does not return a new queryset, but modifies the existing one,
+        to ensure any references to the queryset in the view's context are updated
+        (e.g. when using `context_object_name`).
+        """
+        parent_page_paths = {
+            Page._get_parent_path_from_path(page.path) for page in pages
+        }
+        parent_pages_by_path = {
+            page.path: page
+            for page in Page.objects.filter(path__in=parent_page_paths).specific(
+                defer=True
+            )
+        }
+        for page in pages:
+            parent_page = parent_pages_by_path.get(
+                Page._get_parent_path_from_path(page.path)
+            )
+            page._parent_page = parent_page
+
 
 PageManager = BasePageManager.from_queryset(PageQuerySet)