diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
index 2c96332adf7d00..7363ef34e2730d 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
@@ -284,6 +284,12 @@
     
       Interop\Windows\Kernel32\Interop.DynamicLoad.cs
     
+    
+      Interop\Windows\Kernel32\Interop.QueueUserAPC.cs
+    
+    
+      Interop\Windows\Kernel32\Interop.Threading.cs
+    
     
   
   
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs
index 12a2273eddf605..d8089758b317a7 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs
@@ -20,10 +20,15 @@ public sealed partial class Thread
         [ThreadStatic]
         private static ComState t_comState;
 
+        [ThreadStatic]
+        private static bool t_interruptRequested;
+
         private SafeWaitHandle _osHandle;
 
         private ApartmentState _initialApartmentState = ApartmentState.Unknown;
 
+        private volatile bool _pendingInterrupt;
+
         partial void PlatformSpecificInitialize();
 
         // Platform-specific initialization of foreign threads, i.e. threads not created by Thread.Start
@@ -162,7 +167,40 @@ private bool JoinInternal(int millisecondsTimeout)
                 }
                 else
                 {
-                    result = WaitHandle.WaitOneCore(waitHandle.DangerousGetHandle(), millisecondsTimeout, useTrivialWaits: false);
+                    Thread? currentThread = t_currentThread;
+
+                    // Check for pending interrupt from before thread started
+                    if (currentThread is not null && currentThread._pendingInterrupt)
+                    {
+                        currentThread._pendingInterrupt = false;
+                        throw new ThreadInterruptedException();
+                    }
+
+                    if (currentThread is not null)
+                    {
+                        currentThread.SetWaitSleepJoinState();
+                    }
+
+                    try
+                    {
+                        // Use alertable wait so we can be interrupted by APC
+                        result = (int)Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(),
+                            (uint)millisecondsTimeout, Interop.BOOL.TRUE);
+
+                        // Check if we were interrupted by an APC
+                        if (result == Interop.Kernel32.WAIT_IO_COMPLETION)
+                        {
+                            CheckForInterrupt();
+                            return false; // Interrupted, so join did not complete
+                        }
+                    }
+                    finally
+                    {
+                        if (currentThread is not null)
+                        {
+                            currentThread.ClearWaitSleepJoinState();
+                        }
+                    }
                 }
 
                 return result == (int)Interop.Kernel32.WAIT_OBJECT_0;
@@ -226,6 +264,21 @@ private static uint ThreadEntryPoint(IntPtr parameter)
             return 0;
         }
 
+        private static void CheckPendingInterrupt()
+        {
+            Thread? currentThread = t_currentThread;
+            if (currentThread is not null && currentThread._pendingInterrupt)
+            {
+                currentThread._pendingInterrupt = false;
+                throw new ThreadInterruptedException();
+            }
+        }
+
+        private static void CheckForPendingInterrupt()
+        {
+            CheckPendingInterrupt();
+        }
+
         public ApartmentState GetApartmentState()
         {
             if (this != CurrentThread)
@@ -386,7 +439,51 @@ internal static Thread EnsureThreadPoolThreadInitialized()
             return InitializeExistingThreadPoolThread();
         }
 
-        public void Interrupt() { throw new PlatformNotSupportedException(); }
+        [UnmanagedCallersOnly]
+        private static void InterruptApcCallback(nint parameter)
+        {
+            // This is the native APC callback that sets the interrupt flag
+            // It runs in native code to avoid managed reentrancy issues
+            t_interruptRequested = true;
+        }
+
+        private static void CheckForInterrupt()
+        {
+            if (t_interruptRequested)
+            {
+                t_interruptRequested = false;
+                throw new ThreadInterruptedException();
+            }
+        }
+
+        public void Interrupt()
+        {
+            using (_lock.EnterScope())
+            {
+                // If thread is dead, do nothing
+                if (GetThreadStateBit(ThreadState.Stopped))
+                    return;
+
+                // If thread hasn't started yet, set pending interrupt flag
+                if (GetThreadStateBit(ThreadState.Unstarted))
+                {
+                    _pendingInterrupt = true;
+                    return;
+                }
+
+                // Queue APC to interrupt the thread
+                SafeWaitHandle osHandle = _osHandle;
+                if (osHandle is not null && !osHandle.IsInvalid && !osHandle.IsClosed)
+                {
+                    nint callbackPtr;
+                    unsafe
+                    {
+                        callbackPtr = (nint)(delegate* unmanaged)&InterruptApcCallback;
+                    }
+                    Interop.Kernel32.QueueUserAPC(callbackPtr, osHandle.DangerousGetHandle(), IntPtr.Zero);
+                }
+            }
+        }
 
         internal static bool ReentrantWaitsEnabled =>
             GetCurrentApartmentType() == ApartmentType.STA;
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs
index 3758fca9e8a0a2..a101c27fb23b7d 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs
@@ -46,6 +46,9 @@ public sealed partial class Thread
 
         private static int s_foregroundRunningCount;
 
+        // Platform-specific method to check for pending interrupts when thread starts
+        partial void CheckForPendingInterrupt();
+
         private Thread()
         {
             _managedThreadId = System.Threading.ManagedThreadId.GetCurrentThreadId();
@@ -450,6 +453,9 @@ private static void StartThread(IntPtr parameter)
                 IncrementRunningForeground();
             }
 
+            // Check for any pending interrupt that was queued before the thread started
+            thread.CheckForPendingInterrupt();
+
             try
             {
                 StartHelper? startHelper = thread._startHelper;
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs
new file mode 100644
index 00000000000000..fa36bd32b4922b
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+    internal static partial class Kernel32
+    {
+        internal delegate void PAPCFUNC(nint dwParam);
+
+        [LibraryImport(Libraries.Kernel32, SetLastError = true)]
+        [return: MarshalAs(UnmanagedType.Bool)]
+        internal static partial bool QueueUserAPC(nint pfnAPC, nint hThread, nint dwData);
+    }
+}
\ No newline at end of file
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs
new file mode 100644
index 00000000000000..bab2a3bff71256
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+    internal static partial class Kernel32
+    {
+        [LibraryImport(Libraries.Kernel32)]
+        internal static partial uint SleepEx(uint dwMilliseconds, BOOL bAlertable);
+    }
+}
\ No newline at end of file
diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
index 908d690e7e207d..eaa31ed026a407 100644
--- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
+++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
@@ -916,7 +916,6 @@ public static void LocalDataSlotTest()
 
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
         [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public static void InterruptTest()
         {
             // Interrupting a thread that is not blocked does not do anything, but once the thread starts blocking, it gets
@@ -966,7 +965,6 @@ public static void InterruptTest()
         }
 
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
         public static void InterruptInFinallyBlockTest_SkipOnDesktopFramework()
         {
diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs
index 13cb675cef34ae..1153330d0ec7e1 100644
--- a/src/libraries/System.Threading/tests/MonitorTests.cs
+++ b/src/libraries/System.Threading/tests/MonitorTests.cs
@@ -491,7 +491,6 @@ public static void ObjectHeaderSyncBlockTransitionTryEnterRaceTest()
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
         [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
         [ActiveIssue("https://github.com/dotnet/runtime/issues/87718", TestRuntimes.Mono)]
-        [ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public static void InterruptWaitTest()
         {
             object obj = new();
diff --git a/src/tests/baseservices/threading/regressions/115178/115178.cs b/src/tests/baseservices/threading/regressions/115178/115178.cs
index 34aba004040a9d..e44d860a6b23de 100644
--- a/src/tests/baseservices/threading/regressions/115178/115178.cs
+++ b/src/tests/baseservices/threading/regressions/115178/115178.cs
@@ -288,12 +288,7 @@ public static int TestEntryPoint()
     {
         RunTestUsingInfiniteWait();
         RunTestUsingTimedWait();
-
-        // Thread.Interrupt is not implemented on NativeAOT - https://github.com/dotnet/runtime/issues/69919
-        if (!TestLibrary.Utilities.IsNativeAot)
-        {
-            RunTestInterruptInfiniteWait();
-        }
+        RunTestInterruptInfiniteWait();
 
         return result;
     }