diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 96c285bc0a6c320f117d39ecb06768719da0dddb..9227f07a69d2e36ee2f5e17bc79077695d110979 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -249,6 +249,7 @@
 @import "./views/messages/_MessageActionBar.pcss";
 @import "./views/messages/_MessageTimestamp.pcss";
 @import "./views/messages/_MjolnirBody.pcss";
+@import "./views/messages/_PinnedSeparator.pcss";
 @import "./views/messages/_ReactionsRow.pcss";
 @import "./views/messages/_ReactionsRowButton.pcss";
 @import "./views/messages/_RedactedBody.pcss";
diff --git a/res/css/views/messages/_PinnedSeparator.pcss b/res/css/views/messages/_PinnedSeparator.pcss
new file mode 100644
index 0000000000000000000000000000000000000000..cf1c9495cf45550e36bded592eb764d0a7c2fed1
--- /dev/null
+++ b/res/css/views/messages/_PinnedSeparator.pcss
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.mx_PinnedSeparator {
+    text-align: end;
+    border-bottom: 1px solid var(--cpd-color-icon-accent-primary);
+    font: var(--cpd-font-body-sm-semibold);
+    color: var(--cpd-color-text-action-accent);
+    text-transform: uppercase;
+    margin-bottom: var(--cpd-space-1x);
+}
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 07b600484ac93a0e794421a4cd38f09a17bd84ef..9b704d1574d6372ec0e0118faeacf7fe75151a41 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -56,6 +56,8 @@ import { MainGrouper } from "./grouper/MainGrouper";
 import { CreationGrouper } from "./grouper/CreationGrouper";
 import { _t } from "../../languageHandler";
 import { getLateEventInfo } from "./grouper/LateEventGrouper";
+import PinningUtils from "../../utils/PinningUtils";
+import { PinnedSeparator } from "../views/messages/PinnedSeparator";
 
 const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
 const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
@@ -767,6 +769,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
         }
 
         const cli = MatrixClientPeg.safeGet();
+
+        if (SettingsStore.getValue<boolean>("feature_pinning") && PinningUtils.isPinned(cli, mxEv)) {
+            ret.push(
+                <li key={`${ts1}-pinned`}>
+                    <PinnedSeparator />
+                </li>,
+            );
+        }
+
         let lastInSection = true;
         if (nextEventWithTile) {
             const nextEv = nextEventWithTile;
diff --git a/src/components/views/messages/PinnedSeparator.tsx b/src/components/views/messages/PinnedSeparator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..371a8db1957ff4698a37777b07ae79431ab6664e
--- /dev/null
+++ b/src/components/views/messages/PinnedSeparator.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { JSX } from "react";
+
+import { _t } from "../../../languageHandler";
+
+/**
+ * A separator to be displayed between pinned messages and the rest of the timeline.
+ */
+export function PinnedSeparator(): JSX.Element {
+    return (
+        <div className="mx_PinnedSeparator" role="separator" aria-label={_t("timeline|pinned_separator_description")}>
+            {_t("timeline|pinned_separator")}
+        </div>
+    );
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1b7b1f2ed99122d491e51f6dc34b9ad3caeea8f5..7c6dde7b0be245c8057a2fc6316dff8a94d9bc49 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -3523,6 +3523,8 @@
         "no_permission_messages_before_join": "You don't have permission to view messages from before you joined.",
         "pending_moderation": "Message pending moderation",
         "pending_moderation_reason": "Message pending moderation: %(reason)s",
+        "pinned_separator": "pinned",
+        "pinned_separator_description": "The following message is pinned.",
         "reactions": {
             "add_reaction_prompt": "Add reaction",
             "custom_reaction_fallback_label": "Custom reaction",
diff --git a/src/utils/PinningUtils.ts b/src/utils/PinningUtils.ts
index f9750a3ed2280081f3c49e9f10d0be8aa275c33a..16ec4da45e9d8a2c46a83bf0006d6008bbd758f0 100644
--- a/src/utils/PinningUtils.ts
+++ b/src/utils/PinningUtils.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { MatrixEvent, EventType, M_POLL_START } from "matrix-js-sdk/src/matrix";
+import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix";
 
 export default class PinningUtils {
     /**
@@ -38,4 +38,22 @@ export default class PinningUtils {
 
         return true;
     }
+
+    /**
+     * Determines if the given event is pinned.
+     * @param matrixClient
+     * @param mxEvent
+     */
+    public static isPinned(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
+        const room = matrixClient.getRoom(mxEvent.getRoomId());
+        if (!room) return false;
+
+        const pinnedEvent = room
+            .getLiveTimeline()
+            .getState(EventTimeline.FORWARDS)
+            ?.getStateEvents(EventType.RoomPinnedEvents, "");
+        if (!pinnedEvent) return false;
+        const content = pinnedEvent.getContent();
+        return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(mxEvent.getId());
+    }
 }